﻿/*
 * Catalyst - http://catalystobjects.com
 * 
 * New BSD License (BSD)
 * 
 * Copyright (c) 2009, Ashley Brener
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 * 
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 * 
 * Neither the name of Midlet Earth nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.Reflection;
using System.Xml;
using System.Collections;
using System.ComponentModel;
using SubSonic.Schema;

namespace Catalyst.Xml.Serialization
{
	internal class SerializationContainer
	{
		public PropertyInfo[] Properties { get; set; }
		public Type[] Interfaces { get; set; }
		public bool HasElementType { get; set; }
		public Type ElementType { get; set; }
	}

	public static class XmlSerializer<T>
		where T : new()
	{
		private static Dictionary<Type, SerializationContainer> _serializationContainers;

		private static string _namespace;

		public static XmlQualifiedName ProvideSchema(XmlSchemaSet SchemaSet, string Namespace)
		{
			Type type = typeof(T);

			SchemaSet.Add(GetXmlSchema(Namespace));

			return new XmlQualifiedName(type.Name, Namespace);
		}

		private static SerializationContainer getSerializationContainer(Type type)
		{
			if (_serializationContainers == null)
			{
				_serializationContainers = new Dictionary<Type, SerializationContainer>();
			}

			SerializationContainer container = null;

			if (!_serializationContainers.ContainsKey(type))
			{
				container = new SerializationContainer
				                                   	{
				                                   		Properties = type.GetProperties(),
														Interfaces = type.GetInterfaces(),
														HasElementType = type.HasElementType,
														ElementType = type.GetElementType()
				                                   	};

				_serializationContainers.Add(type, container);
			}
			else
			{
				container = _serializationContainers[type];
			}

			return container;
		}

		public static XmlSchema GetXmlSchema(string Namespace)
		{
			Type type = typeof(T);

			_namespace = Namespace;
			
			// Get base XML Schema
			XmlSchema schema = new XmlSchema
			                   	{
			                   		Id = type.Name,
			                   		TargetNamespace = _namespace,
			                   		ElementFormDefault = XmlSchemaForm.Qualified
			                   	};

			schema.Namespaces.Add("", _namespace);
			schema.Namespaces.Add("tns", _namespace);

			populateSchema(type, schema);

			return schema;
		}

		private static void populateSchema(Type type, XmlSchema schema)
		{
			XmlSchemaComplexType complexType = new XmlSchemaComplexType {Name = type.Name};

			XmlSchemaSequence sequence = new XmlSchemaSequence();

			XmlSchemaElement element = null;

			bool ignore = false;
			string typeName = null;
			string elementName = null;

			foreach (PropertyInfo property in getSerializationContainer(type).Properties)
			{
				ignore = false;

				if (property.Name == "Errors" && property.PropertyType == typeof(List<string>))
				{
					ignore = true;
				}
				else
				{
					foreach (object attribute in property.GetCustomAttributes(typeof (XmlIgnoreAttribute), true))
					{
						if (attribute.GetType() == typeof (XmlIgnoreAttribute))
						{
							ignore = true;
							break;
						}

						if (attribute.GetType() == typeof (XmlElementAttribute))
						{
							elementName = ((XmlElementAttribute) attribute).ElementName;
							break;
						}
					}
				}

				if (!ignore)
				{
					element = new XmlSchemaElement {MinOccurs = 1, MaxOccurs = 1, Name = property.Name};

					if (checkIfValueType(property.PropertyType))
					{
						element.SchemaTypeName = new XmlQualifiedName(getTypeName(property.PropertyType), "http://www.w3.org/2001/XMLSchema");

						if (property.PropertyType == typeof(string))
						{
							element.MinOccurs = 0;
						}
					}
					else
					{
						typeName = injectXmlSchemaTypes(schema, property, elementName);

						element.MinOccurs = 0;
						element.SchemaTypeName = new XmlQualifiedName(typeName, _namespace);
					}

					sequence.Items.Add(element);
				}
			}

			complexType.Particle = sequence;

			//schema.Items.Add(complexType);

			addSchemaType(schema, complexType);
		}

		private static void addSchemaType(XmlSchema schema, XmlSchemaType schemaType)
		{
			XmlSchemaType t = null;

			bool isFound = false;

			foreach (XmlSchemaObject schemaObject in schema.Items)
			{
				t = schemaObject as XmlSchemaType;

				if (t != null)
				{
					if (t.Name == schemaType.Name)
					{
						isFound = true;
						break;
					}
				}
			}

			if (!isFound)
			{
				schema.Items.Add(schemaType);
			}
		}

		private static bool checkIfValueType(Type type)
		{
			if (type.BaseType == typeof(ValueType) || type == typeof(string))
			{
				return true;
			}

			return false;
		}

		private static string injectXmlSchemaTypes(XmlSchema schema, PropertyInfo property, string elementName)
		{
			XmlSchemaType mainSchemaType = getXmlSchemaType(property.PropertyType, elementName);
			//schema.Items.Add(mainSchemaType);

			addSchemaType(schema, mainSchemaType);

			if (getSerializationContainer(property.PropertyType).Interfaces.Contains(typeof(IEnumerable)))
			{
				// Property is an Array

				XmlSchemaType schemaType = null;

				if (property.PropertyType.HasElementType)
				{
					schemaType = getXmlSchemaType(getSerializationContainer(property.PropertyType).ElementType, elementName);

					//schema.Items.Add(schemaType);
				}
				else
				{
					Type type = null;

					if (property.PropertyType.IsGenericType)
					{
						type = property.PropertyType;
					}
					else
					{
						string typeName = String.Format("{0}.{1}", property.PropertyType.Namespace, mainSchemaType.Name.Replace("ArrayOf", ""));

						type = property.PropertyType.Assembly.GetType(typeName);
					}

					schemaType = getXmlSchemaType(type, elementName);

					//schema.Items.Add(schemaType);
				}

				addSchemaType(schema, schemaType);
			}

			return mainSchemaType.Name;
		}

		private static string getTypeName(Type type)
		{
			if (!checkIfValueType(type))
			{
				throw new Exception(String.Format("{0} is not a value type.", type));
			}
	
			XmlSchemas schemas = new XmlSchemas();

			XmlSchemaExporter exporter = new XmlSchemaExporter(schemas);

			XmlTypeMapping mapping = (new XmlReflectionImporter()).ImportTypeMapping(type);

			exporter.ExportTypeMapping(mapping);

			XmlSchemaElement element = (XmlSchemaElement)schemas[0].Items[0];

			string name = element.SchemaTypeName.Name;

			//if (name == "int")
			//{
			//    name = "integer";
			//}

			return name;
		}

		private static XmlSchemaType getXmlSchemaType(Type type, string elementName)
		{
			XmlSchemas schemas = new XmlSchemas();

			XmlSchemaExporter exporter = new XmlSchemaExporter(schemas);

			XmlTypeMapping mapping = (new XmlReflectionImporter(_namespace)).ImportTypeMapping(type);
			exporter.ExportTypeMapping(mapping);

			XmlSchemaType schemaType = (XmlSchemaType)schemas[0].Items[1];

			if (!String.IsNullOrEmpty(elementName))
			{
				// ElementName overridden by XmlElementAttribute

				schemaType.Name = elementName;
			}

			return schemaType;
		}

		public static void Deserialize(XmlReader Reader, T Item)
		{
			Type type = typeof(T);

			PropertyInfo[] properties = type.GetProperties();
			
			string elementName = null;
			bool ignore;

			TypeConverter typeConverter = null;

			while (Reader.Read())
			{
				foreach (PropertyInfo property in properties)
				{
					elementName = property.Name;
					ignore = false;

					// Check XmlAttributes
					foreach (object attribute in property.GetCustomAttributes(typeof(XmlIgnoreAttribute), true))
					{
						if (attribute.GetType() == typeof(XmlIgnoreAttribute))
						{
							ignore = true;
							break;
						}

						if (attribute.GetType() == typeof(XmlElementAttribute))
						{
							elementName = ((XmlElementAttribute)attribute).ElementName;
							break;
						}
					}
		
					if (!ignore)
					{
						if (Reader.Name == elementName)
						{
							typeConverter = TypeDescriptor.GetConverter(property.GetType());
							property.SetValue(Item, typeConverter.ConvertFromString(Reader.ReadString()), null);
						}
					}
				}
			}
		}

		public static void Serialize(XmlWriter Writer, T Item)
		{
			Type type = typeof(T);

			string elementName = null;
			bool ignore;

			foreach (PropertyInfo property in type.GetProperties())
			{
				elementName = property.Name;
				ignore = false;

				if (property.PropertyType.IsNotPublic
					|| (Item is IActiveRecord && (property.Name == "IsDeleted" || property.Name == "TestMode")))
				{
					ignore = true;
				}
				else
				{
					// Check XmlAttributes
					foreach (object attribute in property.GetCustomAttributes(typeof (XmlIgnoreAttribute), true))
					{
						if (attribute.GetType() == typeof (XmlIgnoreAttribute))
						{
							ignore = true;
							break;
						}

						if (attribute.GetType() == typeof (XmlElementAttribute))
						{
							elementName = ((XmlElementAttribute) attribute).ElementName;
							break;
						}
					}
				}

				if (!ignore)
				{
					serialize(Writer, property.PropertyType, elementName, property.GetValue(Item, null));
				}
			}
		}

		private static void serialize(XmlWriter writer, Type type, string elementName, object obj)
		{
			XmlSerializer serializer = null;
			Type[] interfaces = getSerializationContainer(type).Interfaces;

			writer.WriteStartElement(elementName);

			if (checkIfValueType(type) || type.IsEnum)
			{
				string value = string.Empty;

				if (type == typeof(Boolean))
				{
					value = XmlConvert.ToString((bool)obj);
				}
				else if (type == typeof(DateTime))
				{
					value = XmlConvert.ToString(Convert.ToDateTime(obj), XmlDateTimeSerializationMode.Unspecified);
				}
				else if (obj != null)
				{
					value = obj.ToString();
				}

				writer.WriteString(value);
			}
			//else if (interfaces.Contains(typeof(IXmlSerializable)))
			//{
			//    try
			//    {
			//        ((IXmlSerializable)obj).WriteXml(writer);
			//    }
			//    catch (Exception ex)
			//    {
			//        throw new Exception(String.Format("Could not serialize Relation \"{0}\" for BusinessObject \"{1}\".\r\n\r\n{2}\r\n{3}", elementName, obj.GetType().FullName, ex.Message, ex.StackTrace));
			//    }
			//}
			else if (interfaces.Contains(typeof(IEnumerable)))
			{
				IEnumerator enumerator = ((IEnumerable)obj).GetEnumerator();

				while (enumerator.MoveNext())
				{
					serialize(writer, enumerator.Current.GetType(), enumerator.Current.GetType().Name, enumerator.Current);
				}
			}
			else
			{
				serializer = new XmlSerializer(type);
				serializer.Serialize(writer, obj);
			}

			writer.WriteEndElement();
		}
	}

}
